PostCSS: будущее после Sass и Less

Андрей Ситник, Злые марсиане

PostCSS Будущее после Sass и Less

logo.svg Андрей Ситник, Злые марсиане

martians.svg

Работал над

ebay.svggroupon.svgamplifr.png

Опенсорс

autoprefixer.svglogo.svg

Часть 1 Проблема

covers/problem.jpg

Эволюция

Мутации
Отбор
Наследование

Естественный отбор?

<blink> поддерживали 19 лет

Случайные мутации

Свобода ничего не стоит, если она не включает в себя свободу ошибаться

— Махатма Ганди

40% ES6 пришло из CoffeeScript

Часть 2 Препроцессоры

covers/sass.jpg

Шаблонизаторы CSS

a {
    <%= include clickable %>
    color: <%= $link-color %>;
}

Синтаксические возможности

  1. Переменные
    $var: 1
  2. Примеси
    @include mixin
  3. Функции
    black()

Проблема 1 Ограниченность

a {
    width: 20rem;
}

Проблема 2 Монолитность

Проблема 3 Неудобно программировать

@import "compass/support";

// The the user threshold for transition support. Defaults to `$graceful-usage-threshold`
$transition-support-threshold: $graceful-usage-threshold !default;


// CSS Transitions
// Currently only works in Webkit.
//
// * expected in CSS3, FireFox 3.6/7 and Opera Presto 2.3
// * We'll be prepared.
//
// Including this submodule sets following defaults for the mixins:
//
//     $default-transition-property : all
//     $default-transition-duration : 1s
//     $default-transition-function : false
//     $default-transition-delay    : false
//
// Override them if you like. Timing-function and delay are set to false for browser defaults (ease, 0s).

$default-transition-property: all !default;

$default-transition-duration: 1s !default;

$default-transition-function: null !default;

$default-transition-delay: null !default;

$transitionable-prefixed-values: transform, transform-origin !default;



// Checks if the value given is a unit of time.
@function is-time($value) {
  @return if(type-of($value) == number, not not index(s ms, unit($value)), false);
}

// Returns `$property` with the given prefix if it is found in `$transitionable-prefixed-values`.
@function prefixed-for-transition($prefix, $property) {
  @if not $prefix {
    @return $property;
  }
  @if type-of($property) == list or type-of($property) == arglist {
    $new-list: comma-list();
    @each $v in $property {
      $new-list: append($new-list, prefixed-for-transition($prefix, $v));
    }
    @return $new-list;
  } @else {
    @if index($transitionable-prefixed-values, $property) {
      @return #{$prefix}-#{$property};
    } @else {
      @return $property;
    }
  }
}

// Returns $transition-map which includes key and values that map to a transition declaration
@function transition-map($transition) {
  $transition-map: ();

  @each $item in $transition {
    @if is-time($item) {
      @if map-has-key($transition-map, duration) {
        $transition-map: map-merge($transition-map, (delay: $item));
      } @else {
        $transition-map: map-merge($transition-map, (duration: $item));
      }
    } @else if map-has-key($transition-map, property) {
      $transition-map: map-merge($transition-map, (timing-function: $item));
    } @else {
      $transition-map: map-merge($transition-map, (property: $item));
    }
  }

  @return $transition-map;
}

// One or more properties to transition
//
// * for multiple, use a comma-delimited list
// * also accepts "all" or "none"

@mixin transition-property($properties...) {
  $properties: set-arglist-default($properties, $default-transition-property);
  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $props: if($current-prefix, prefixed-for-transition($current-prefix, $properties), $properties);
    @include prefix-prop(transition-property, $props);
  }
}

// One or more durations in seconds
//
// * for multiple, use a comma-delimited list
// * these durations will affect the properties in the same list position

@mixin transition-duration($durations...) {
  $durations: set-arglist-default($durations, $default-transition-duration);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-duration: $durations
  ));
}

// One or more timing functions
//
// * [ ease | linear | ease-in | ease-out | ease-in-out | cubic-bezier(x1, y1, x2, y2)]
// * For multiple, use a comma-delimited list
// * These functions will effect the properties in the same list position

@mixin transition-timing-function($functions...) {
  $functions: set-arglist-default($functions, $default-transition-function);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-timing-function: $functions
  ));
}

// One or more transition-delays in seconds
//
// * for multiple, use a comma-delimited list
// * these delays will effect the properties in the same list position

@mixin transition-delay($delays...) {
  $delays: set-arglist-default($delays, $default-transition-delay);
  @include prefixed-properties(css-transitions, $transition-support-threshold, (
    transition-delay: $delays
  ));
}

// Transition all-in-one shorthand

@mixin single-transition(
  $property: $default-transition-property,
  $duration: $default-transition-duration,
  $function: $default-transition-function,
  $delay: $default-transition-delay
) {
  @include transition(compact($property $duration $function $delay));
}

@mixin transition($transitions...) {
  $default: (compact($default-transition-property $default-transition-duration $default-transition-function $default-transition-delay),);
  $transitions: if(length($transitions) == 1 and type-of(nth($transitions, 1)) == list and list-separator(nth($transitions, 1)) == comma, nth($transitions, 1), $transitions);
  $transitions: set-arglist-default($transitions, $default);


  @include with-each-prefix(css-transitions, $transition-support-threshold) {
    $delays: comma-list();
    $transitions-without-delays: comma-list();
    $transitions-with-delays: comma-list();
    $has-delays: false;


    // This block can be made considerably simpler at the point in time that
    // we no longer need to deal with the differences in how delays are treated.
    @each $transition in $transitions {
      // Declare initial values for transition
      $transition: transition-map($transition);

      $property: map-get($transition, property);
      $duration: map-get($transition, duration);
      $timing-function: map-get($transition, timing-function);
      $delay: map-get($transition, delay);

      // Parse transition string to assign values into correct variables
      $has-delays: $has-delays or $delay;

      @if $current-prefix == -webkit {
        // Keep a list of delays in case one is specified
        $delays: append($delays, if($delay, $delay, 0s));
        $transitions-without-delays: append($transitions-without-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function);
      } @else {
        $transitions-with-delays: append($transitions-with-delays,
          prefixed-for-transition($current-prefix, $property) $duration $timing-function $delay);
      }
    }

    @if $current-prefix == -webkit {
      @include prefix-prop(transition, $transitions-without-delays);
      @if $has-delays {
        @include prefix-prop(transition-delay, $delays);
      }
    } @else if $current-prefix {
      @include prefix-prop(transition, $transitions-with-delays);
    } @else {
      transition: $transitions-with-delays;
    }
  }
}

Часть 3 PostCSS

covers/postcss.jpg

Начало

tj.jpg «Модульный CSS-препроцессинг с Реворком»
— TJ Holowaychuk, 2013

Развитие

Rework

PostCSS

PostCSS

CSS
карта кода
Парсер
Плагин
Плагин
Сохранение
Новый CSS
новая карта

Использование

let postcss = require('postcss');

postcss([ plugin1, plugin2 ])
    .process(css)
    .then( result => console.log(result.css) );

Плагин

function (css) {
    css.eachDecl( decl => {
        decl.value = decl.value.replace(/\d+rem/, rem => {
            return 16 * parseFloat(rem) + 'px';
        });
    });
};

Разница

Препроцессор

PostCSS

Эволюция

Пишем плагин
Популярность
Спецификация

Часть 4 Практика

covers/practice.jpg

Плагины postcss-simple-vars

$blue: #056ef0;
$column: 200px;

.menu_link {
    background: $blue;
    width: $column;
}

Плагины postcss-nested

.phone {
    &_title {
        width: 500px;
        @media (max-width: 500px) {
            width: auto;
        }
    }
}

Плагины postcss-mixins

@define-mixin icon $network $color {
    .icon.is-$network {
        color: $color;
    }
}

@mixin icon twitter blue;
@mixin icon youtube red;

Поддерживаемость

Невозможно на Sass autoprefixer

:fullscreen a {
    transition: transform 1s;
}
:-webkit-full-screen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s;
}
:-moz-full-screen a {
    transition: transform 1s;
}
:-ms-fullscreen a {
    transition: transform 1s;
}
:fullscreen a {
    -webkit-transition: -webkit-transform 1s;
            transition: transform 1s;
}

Невозможно на Sass cssnext

@custom-selector --heading h1, h2, h3, h4, h5, h6;

.post-article --heading {
    margin-top: calc(10 * var(--row));
    color: color( var(--mainColor) blackness(+20%) );
    font-variant-caps: small-caps;
}

Невозможно на Sass cssgrace

.icon {
    opacity: 0.6;
    display: inline-block;
}
.icon {
    opacity: 0.6;
    filter: alpha(opacity=60);
    display: inline-block;
    *display: inline;
    *zoom: 1;
}

Невозможно на Sass postcss-quantity-queries

ul > li:exactly(4) {
    width: 25%;
}

ul > li:exactly(5) {
    width: 20%;
}
ul > li:nth-last-child(4):first-child,
ul > li:nth-last-child(4):first-child ~ li {
    width: 25%;
}


ul > li:nth-last-child(5):first-child,
ul > li:nth-last-child(5):first-child ~ li {
    width: 20%;
}

Невозможно на Sass postcss-data-packer

/* style.css */
.icon1 {
    width: 100px;
    background: url(data:…);
}
.icon2 {
    background: url(data:…);
}
/* style.css */
.icon1 {
    width: 100px;
}
/* style.icons.css */
.icon1, .icon2 {
    background: url(data:…);
}

Невозможно на Sass postcss-bem-linter

Проверяет БЭМ для Твиттера (методология SUIT CSS)

Невозможно на Sass doiuse

Проверяет поддержку свойств в нужных браузерах по Can I Use

main.css: line 15, col 3 -
  CSS user-select: none not supported by: IE (8,9)
main.css: line 32, col 3 -
  CSS3 Transforms not supported by: IE (8)

Википедия на иврите

hewiki.jpg

Невозможно на Sass rtlcss

Изменяет дизайн для арабского и иврита

a {
    left: 10px;
    text-align: left;
}
a {
    right: 10px;
    text-align: right;
}

Ещё более 70 плагинов

Скорость

PostCSS
36 мс
libsass
109 мс
Less
150 мс
Stylus
283 мс
Sass
1153 мс

Скорость

PostCSS
36 мс
libsass
109 мс
Less
150 мс
Stylus
283 мс
Sass
1153 мс

Преимущества

  1. Скорость
  2. Модульность
  3. Функции, невозможные на Sass

Часть 5 Настоящее

covers/present.jpg

430 000 загрузок в месяц

Используют PostCSS

google.svgtaobao.svgwordpress.svgtwitter.svg

Обсуждение

alistapart.png «Что спасёт нас от тёмной стороны CSS‑препроцессоров»
 — A List Apart

Обсуждение

benfrain.jpg «Расставание с Sass: дело во мне, а не в тебе»
 — Бен Фрейн, автор «Sass и Compass для дизайнеров»

Часть 6 Применяем

covers/use.jpg

1. Разработка CSS-инструмента

2. Сайт не использует PostCSS

.pipe( sass() )
.pipe( postcss([
    require('autoprefixer')
]) )

3. Сайт уже с Автопрефиксером

.pipe( sass() )
.pipe( postcss([
    require('autoprefixer'),
    require('postcss-easings'),
    require('cssnext')
]) )

4. Новый проект

.pipe( postcss([
    require('postcss-nested'),
    require('postcss-mixins'),
    require('postcss-simple-vars'),
    require('autoprefixer'),
    require('postcss-easings'),
    require('cssnext')
]) )

Вопросы

covers/ask.jpg